﻿using System.Collections;
using System.Collections.Generic;
using UnityEngine;


/**
 * Sample component that shows how to use Obi Rope to create a grappling hook for a 2.5D game.
 * 95% of the code is the grappling hook logic (user input, scene raycasting, launching, attaching the hook, etc) and parameter setup,
 * to show how to use Obi completely at runtime. This might not be practical for real-world scenarios,
 * but illustrates how to do it.
 *
 * Note that the choice of using actual rope simulation for grapple dynamics is debatable. Usually
 * a simple spring works better both in terms of performance and controllability. 
 *
 * If complex interaction is required with the scene, a purely geometry-based approach (ala Worms ninja rope) can
 * be the right choice under certain circumstances.
 */

namespace Obi.Samples
{
    public class ExtendableGrapplingHook : MonoBehaviour
    {

        public ObiSolver solver;
        public ObiCollider character;

        public Material material;
        public ObiRopeSection section;

        [Range(0, 1)]
        public float hookResolution = 0.5f;
        public float hookExtendRetractSpeed = 2;
        public float hookShootSpeed = 30;
        public int particlePoolSize = 100;

        private ObiRope rope;
        private ObiRopeBlueprint blueprint;
        private ObiRopeExtrudedRenderer ropeRenderer;

        private ObiRopeCursor cursor;

        private RaycastHit hookAttachment;

        void Awake()
        {

            // Create both the rope and the solver:	
            rope = gameObject.AddComponent<ObiRope>();
            ropeRenderer = gameObject.AddComponent<ObiRopeExtrudedRenderer>();
            ropeRenderer.section = section;
            ropeRenderer.uvScale = new Vector2(1, 4);
            ropeRenderer.normalizeV = false;
            ropeRenderer.uvAnchor = 1;
            ropeRenderer.material = material;

            // Setup a blueprint for the rope:
            blueprint = ScriptableObject.CreateInstance<ObiRopeBlueprint>();
            blueprint.resolution = 0.5f;
            blueprint.pooledParticles = particlePoolSize;

            // Tweak rope parameters:
            rope.maxBending = 0.02f;

            // Add a cursor to be able to change rope length:
            cursor = rope.gameObject.AddComponent<ObiRopeCursor>();
            cursor.cursorMu = 0;
            cursor.direction = true;
        }

        private void OnDestroy()
        {
            DestroyImmediate(blueprint);
        }

        /**
         * Raycast against the scene to see if we can attach the hook to something.
         */
        private void LaunchHook()
        {

            // Get the mouse position in the scene, in the same XY plane as this object:
            Vector3 mouse = Input.mousePosition;
            mouse.z = transform.position.z - Camera.main.transform.position.z;
            Vector3 mouseInScene = Camera.main.ScreenToWorldPoint(mouse);

            // Get a ray from the character to the mouse:
            Ray ray = new Ray(transform.position, mouseInScene - transform.position);

            // Raycast to see what we hit:
            if (Physics.Raycast(ray, out hookAttachment))
            {
                // We actually hit something, so attach the hook!
                StartCoroutine(AttachHook());
            }

        }

        private void LayParticlesInStraightLine(Vector3 origin, Vector3 direction)
        {
            // placing all particles in a straight line, respecting rope length
            float length = 0;
            for (int i = 0; i < rope.elements.Count; ++i)
            {
                int p1 = rope.elements[i].particle1;
                int p2 = rope.elements[i].particle2;

                solver.prevPositions[p1] = solver.positions[p1] = origin + direction * length;
                length += rope.elements[i].restLength;
                solver.prevPositions[p2] = solver.positions[p2] = origin + direction * length;
            }
        }

        private IEnumerator AttachHook()
        {
            yield return null;

            // Clear pin constraints:
            var pinConstraints = rope.GetConstraintsByType(Oni.ConstraintType.Pin) as ObiConstraints<ObiPinConstraintsBatch>;
            pinConstraints.Clear();

            Vector3 localHit = rope.transform.InverseTransformPoint(hookAttachment.point);

            // Procedurally generate the rope path (just a short segment, as we will extend it over time):
            int filter = ObiUtils.MakeFilter(ObiUtils.CollideWithEverything, 0);
            blueprint.path.Clear();
            blueprint.path.AddControlPoint(Vector3.zero, Vector3.zero, Vector3.zero, Vector3.up, 0.1f, 0.1f, 1, filter, Color.white, "Hook start");
            blueprint.path.AddControlPoint(localHit.normalized * 0.5f, Vector3.zero, Vector3.zero, Vector3.up, 0.1f, 0.1f, 1, filter, Color.white, "Hook end");
            blueprint.path.FlushEvents();

            // Generate the particle representation of the rope (wait until it has finished):
            yield return blueprint.Generate();

            // Set the blueprint (this adds particles/constraints to the solver and starts simulating them).
            rope.ropeBlueprint = blueprint;
            rope.GetComponent<ObiRopeExtrudedRenderer>().enabled = true;

            // wait for the solver to load the rope, after the next physics step:
            yield return new WaitForFixedUpdate();
            yield return null;

            // set masses to zero, as we're going to override positions while we extend the rope:
            for (int i = 0; i < rope.activeParticleCount; ++i)
                solver.invMasses[rope.solverIndices[i]] = 0;

            // while the last particle hasn't reached the hit, extend the rope:
            Vector3 origin;
            Vector3 direction;

            while (true)
            {
                // calculate rope origin in solver space:
                origin = solver.transform.InverseTransformPoint(rope.transform.position);

                // update direction and distance to hook point:
                direction = solver.transform.InverseTransformPoint(hookAttachment.point) - origin;
                float distance = direction.magnitude;
                direction.Normalize();

                LayParticlesInStraightLine(origin, direction);

                // increase length:
                float distanceLeft = distance - cursor.ChangeLength(hookShootSpeed * Time.deltaTime);

                // if we have exceeded the desired length, correct it and break the loop:
                if (distanceLeft < 0)
                {
                    cursor.ChangeLength(distanceLeft);
                    break;
                }

                // wait for next frame:
                yield return null;
            }

            // wait for the last length change to take effect, and ensure the rope is straight:
            yield return new WaitForFixedUpdate();
            yield return null;
            LayParticlesInStraightLine(origin, direction);

            // restore masses so that the simulation takes over now that the rope is in place:
            for (int i = 0; i < rope.activeParticleCount; ++i)
                solver.invMasses[rope.solverIndices[i]] = 10; // 1/0.1 = 10

            // Pin both ends of the rope (this enables two-way interaction between character and rope):
            var batch = new ObiPinConstraintsBatch();
            batch.AddConstraint(rope.elements[0].particle1, character, transform.localPosition, Quaternion.identity, 0, 0);
            batch.AddConstraint(rope.elements[rope.elements.Count - 1].particle2, hookAttachment.collider.GetComponent<ObiColliderBase>(),
                                                              hookAttachment.collider.transform.InverseTransformPoint(hookAttachment.point), Quaternion.identity, 0, 0);
            batch.activeConstraintCount = 2;
            pinConstraints.AddBatch(batch);

            rope.SetConstraintsDirty(Oni.ConstraintType.Pin);
        }

        private void DetachHook()
        {
            // Set the rope blueprint to null (automatically removes the previous blueprint from the solver, if any).
            rope.ropeBlueprint = null;
            rope.GetComponent<ObiRopeExtrudedRenderer>().enabled = false;
        }


        void Update()
        {

            if (Input.GetMouseButtonDown(0))
            {
                if (!rope.isLoaded)
                    LaunchHook();
                else
                    DetachHook();
            }

            if (rope.isLoaded)
            {
                if (Input.GetKey(KeyCode.W))
                {
                    cursor.ChangeLength(-hookExtendRetractSpeed * Time.deltaTime);
                }
                if (Input.GetKey(KeyCode.S))
                {
                    cursor.ChangeLength(hookExtendRetractSpeed * Time.deltaTime);
                }
            }
        }
    }
}